/// ===========================================================================
/// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY
/// KIND, WHETHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
/// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR
/// PURPOSE.
/// ===========================================================================
/// 
/// Project:        MOSS Faceted Search
/// Author:         Leonid Lyublinski (leonidly@microsoft.com)
/// Company:        Microsoft Services
/// Date:           05/04/2007  Version:        1.0
///
/// ===========================================================================

using System;
using System.IO;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.Xml.Serialization;
using System.Data;
using System.Threading;
using Microsoft.SharePoint.WebPartPages;
using Microsoft.Office.Server;
using Microsoft.Office.Server.Search.Administration;
using Microsoft.Office.Server.Search.Query;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;

namespace Microsoft.SharePoint.Portal.ExtendedSearch.WebControls
{
    /// <summary>
    /// Custom faceted search web part.
    /// The web part has a series of shared web part properties that can be used
    /// to control the query visually and functionally.
    /// </summary>
    [System.Web.AspNetHostingPermission(System.Security.Permissions.SecurityAction.Demand, Level = System.Web.AspNetHostingPermissionLevel.Minimal)]
    [System.Web.AspNetHostingPermission(System.Security.Permissions.SecurityAction.InheritanceDemand, Level = System.Web.AspNetHostingPermissionLevel.Minimal)]
    [System.Security.Permissions.SecurityPermission(System.Security.Permissions.SecurityAction.Demand, Execution = true)]
    //[System.Security.Permissions.RegistryPermission(System.Security.Permissions.SecurityAction.Assert, Unrestricted = true)]
    //[Microsoft.SharePoint.Security.WebPartPermission(System.Security.Permissions.SecurityAction.Demand, Connections = true)]
    [ToolboxData("<{0}:SearchFacets runat=server></{0}:SearchFacets>")]
    [XmlRoot(Namespace = "Microsoft.SharePoint.Portal.ExtendedSearch")]
    public class SearchFacets : System.Web.UI.WebControls.WebParts.WebPart, ICallbackEventHandler
    {
        #region Constants
        internal const string ERROR_LOADING_XML = "Error loading facets as Xml. Check value of \"Select Columns for facets property\".";
        internal const string ERROR_CREATING_CONTROLS = "Error creating child controls.";
        internal const string ERROR_LOADING_FACETS = "Managed properties don't contain the facet: ";
        internal const string ERROR_CONVERTING_ORDERBY = "The OrderBy in XML property assigned an invalid value.";
        internal const string ERROR_CONVERTING_MAXRESULTS = "The MaxResults in XML property assigned an invalid value.";
        internal const string key = "FacetedSearch";
        #endregion

        #region Fields
        private short _resultsPerPage;// = 500;
        private string _selectColumns;// = @"<root xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""><Columns><Column Name=""Path"" /></Columns></root>";
        private string _hiddenRule;
        private XmlDocument _xmlColumns;
        private FacetMenu _facets;
        private string _scope;
        private string _k;
        private string _sql;
        private int _start;
        private string _v;
        private NameValueCollection _properties;
        private bool _showEmpty;
        private bool _prefixEnabled;


        private string _facetNameCss;// = "ms-navheader";
        private string _facetNameContainerCss;// = "ms-quicklaunch";
        private string _facetValueCss;// = "ms-navitem";
        private string _facetValueContainerCss;// = "ms-navSubMenu2";
        private string _listCss;// = "ms-navsubmenu1";
        private string _listFacetsCss;// = "ms-navsubmenu1";
        private short _cropMax;// = 20;
        private string _facetListId;

        //private Dictionary<string, int> _facetTables;

        private string _facetHtml;
        private bool _run2ndTime;
        private bool _running2ndTime;
        private bool _moreResultsThanFacets;
        private bool _isInPreviewMode;
        private bool _isEditOrDesignMode;
        private ClientScriptManager csm;
        #endregion

        #region Properties
        /// <summary>
        /// Number of results returned by the search engine in one resultset.
        /// The higher value, the more accurate is the calculation of facet hits.
        /// </summary>
        /// <remarks>High value will result in performance penalty.</remarks>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Total Results")]
        [Description("Number of results returned by the search to selectively define facets.")]
        public short ResultsPerPage
        {
            get
            {
                return _resultsPerPage;
            }
            set
            {
                _resultsPerPage = value;
            }
        }

        /// <summary>
        /// Selected columns are defined as managed properties and will be available as search facets. 
        /// The columns must be crawled and set as managed properties in the SSP administration.
        /// </summary>
        /// <remarks>Test against existing managed properties is built-in, however a proper exception propagation is still required.</remarks>
        /// <permission cref=""></permission>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Select Columns for Facets")]
        [Description("Selected columns should be defined as managed properties BEFORE. They will get available as search facets.")]
        public string SelectColumns
        {
            get
            {
                return _selectColumns;
            }
            set
            {
                _selectColumns = value;                          
            }
        }

        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Hidden Rule")]
        [Description("A property assignment that will be always added to the search criteria. Useful for custom scopes. Format as in the Rule <prop name> = <prop value>.")]
        public string HiddenRule
        {
            get { return _hiddenRule; }
            set { _hiddenRule = value; }
        }
        
        /// <summary>
        /// The CSS class for the list of facets.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("List CSS Class")]
        [Description("Select CSS class for the entire list.")]
        public string ListCss
        {
            get
            {
                return _listCss;
            }
            set
            {
                _listCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the face name.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Facet Name Container CSS Class")]
        [Description("Select CSS class for the facet header container .")]
        public string FacetNameContainerCss
        {
            get
            {
                return _facetNameContainerCss;
            }
            set
            {
                _facetNameContainerCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the face name.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Facet CSS Class")]
        [Description("Select CSS class for the facet header.")]
        public string FacetNameCss
        {
            get
            {
                return _facetNameCss;
            }
            set
            {
                _facetNameCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the list of facets.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Facets List CSS Class")]
        [Description("Select CSS class for the entire list of facets.")]
        public string FacetsListCss
        {
            get
            {
                return _listFacetsCss;
            }
            set
            {
                _listFacetsCss = value;
            }
        }
        /// <summary>
        /// The CSS class for the face value.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Facet Value Container CSS Class")]
        [Description("Select CSS class for the facet value container .")]
        public string FacetValueContainerCss
        {
            get
            {
                return _facetValueContainerCss;
            }
            set
            {
                _facetValueContainerCss = value;
            }
        }

        /// <summary>
        /// The CSS class for the face value and # of hits.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Hit CSS Class")]
        [Description("Select CSS class for the facet value.")]
        public string FacetValueCss
        {
            get
            {
                return _facetValueCss;
            }
            set
            {
                _facetValueCss = value;
            }
        }


        /// <summary>
        /// Maximum number of characters in a facet before truncation. After the facet value reached the maximum, 
        /// it is truncated, "..." aded to the end. Also, ToolTip is added for long values with a full text.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Facet Length")]
        [Description("Maximum number of characters in a facet before truncation.")]
        public short CropMax
        {
            get
            {
                return _cropMax;
            }
            set
            {
                _cropMax = value;
            }
        }

        /// <summary>
        /// Will cause show facets that don't have values.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Show Empty Facet")]
        [Description("Will cause show facets that don't have values.")]        
        public bool ShowEmptyFacet
        {
            get { return _showEmpty; }
            set { _showEmpty = value; }
        }

        /// <summary>
        /// Adds prefix search to the Facet match.
        /// </summary>
        [WebBrowsable(true)]
        [Category("Results Display")]
        [PersistenceMode(PersistenceMode.InnerProperty)]
        [Personalizable(PersonalizationScope.Shared)]
        [FriendlyName("Prefix search")]
        [Description("Will use prefix search in addition to the default suffix search.")]  
        public bool PrefixEnabled
        {
            get { return _prefixEnabled; }
            set { _prefixEnabled = value; }
        }
        #endregion

        #region Delegates
        /// <summary>
        /// The method fires for each bound DataList item and dynamically builds its content.
        /// Building of the content is done via population of Controls collection.
        /// </summary>
        /// <param name="sender">DataList used as a placeholder for facets.</param>
        /// <param name="e"><typeparamref name="DataListItemEventArgs"/></param>
        /// <remarks>Each DataList item represents one Facet, rendered as a label and a set of Facet Values, rendered as a datalist.</remarks>
        protected void rpt_ItemDataBound(object sender, DataListItemEventArgs e)
        {
            DataListItem item = e.Item;
            DataList list = new DataList();
            list.CssClass = _listFacetsCss;

            FacetCollection facetCollection = (FacetCollection)item.DataItem;
            list.ID = facetCollection.Name;
            list.Attributes.Add("facet", facetCollection.Name);
            list.Attributes.Add("count", facetCollection.Count.ToString());
            list.Attributes.Add("totalresults", GetColumnAttribute(facetCollection.Name, "TotalResults"));
            list.ItemCommand += new DataListCommandEventHandler(list_ItemCommand);
            list.ShowFooter = false;
            list.ShowHeader = true;

            // facet name
            string facetCollectionName = (GetColumnAttribute(facetCollection.Name, "DisplayName") == null) ? facetCollection.Name : GetColumnAttribute(facetCollection.Name, "DisplayName");
            list.HeaderTemplate = new HeaderTemplate(ListItemType.Header, _facetNameCss, facetCollection.Name, facetCollectionName, string.Format("total facets:{0}, total hits:{1}, top hit:{2}", facetCollection.Count, facetCollection.Hits, facetCollection.Max), -1, facetCollection.Count, _run2ndTime && _moreResultsThanFacets && !_running2ndTime);
            list.HeaderStyle.Width = Unit.Percentage(100);
            list.HeaderStyle.CssClass = _facetNameContainerCss;
            
            // facet value
            if (_run2ndTime && _running2ndTime)
                list.ItemTemplate = new FacetLightTemplate(ListItemType.Item, _facetValueCss, _cropMax, facetCollection.Name);
            else
                list.ItemTemplate = new FacetTemplate(ListItemType.Item, _facetValueCss, _cropMax);
            list.ItemStyle.Width = Unit.Percentage(100);
            list.ItemStyle.CssClass = _facetValueContainerCss;


            // list.CssClass = _listCss;
            list.Width = Unit.Percentage(100);
            list.DataSource = facetCollection;

            // adding sorting
            string orderBy = GetColumnAttribute(facetCollection.Name, "OrderBy");
            if (!string.IsNullOrEmpty(orderBy))
            {
                try
                {
                    FacetSortEnum facetOrderBy = FacetSortEnum.None;
                    EnumConverter converter = (EnumConverter)TypeDescriptor.GetConverter(facetOrderBy);
                    facetOrderBy = (FacetSortEnum)converter.ConvertFromString(orderBy);
                    facetCollection.SortOrder = facetOrderBy;
                    facetCollection.Sort(facetCollection);
                }
                catch
                {
                    throw new NotSupportedException(ERROR_CONVERTING_ORDERBY);
                }
            }

            // adding paging
            string maxResults = GetColumnAttribute(facetCollection.Name, "TotalResults");
            if (!string.IsNullOrEmpty(maxResults))
            {
                int totalResults;
                if (int.TryParse(maxResults, out totalResults))
                {
                    if (totalResults > -1 && facetCollection.Count > totalResults) // means all results
                    {
                        list.HeaderTemplate = new HeaderTemplate(ListItemType.Header, _facetNameCss, facetCollection.Name, facetCollectionName, string.Format("total facets:{0}, total hits:{1}, top hit:{2}", facetCollection.Count, facetCollection.Hits, facetCollection.Max), totalResults, facetCollection.Count, _run2ndTime && _moreResultsThanFacets && !_running2ndTime);

                        // review styles if new web part properties needed
                        list.FooterTemplate = new FooterTemplate(ListItemType.Footer, _facetValueCss, list.ID, totalResults);
                        list.FooterStyle.Width = Unit.Percentage(100);
                        list.FooterStyle.CssClass = _facetValueContainerCss;
                        list.ShowFooter = true;

                        //_facetTables.Add(list.ID, totalResults);
                    }
                }
            }

            list.DataBind();
            item.Controls.Add(list);


            //item.Controls.Add(new LiteralControl("<BR>"));
        }

        protected void list_ItemDataBound(object sender, DataListItemEventArgs e)
        {
            DataListItem item = e.Item;
            int i = item.ItemIndex;
            DataList list = (DataList)sender;
            string id = list.ID;
            string totalResults = GetColumnAttribute(list.ID, "TotalResults");
            if (!string.IsNullOrEmpty(totalResults))
            {
                int totalResultsInt = int.Parse(totalResults);
                if (i > totalResultsInt)
                    item.Attributes.Add("visible", "true");
                else
                    item.Attributes.Add("visible", "false");
            }

        }

        /// <summary>
        /// The method fires when the Facet link clicked in the facet menu. Functionality is repsonsible for refining the search results.
        /// The code parses Facet and its value, reconstructs the querystring and redirects the user to a constructed query.
        /// </summary>
        /// <param name="source">DataList</param>
        /// <param name="e"><typeparamref name="DataListCommandEventArgs"/></param>
        /// <remarks>Currently implemented as a postback. Consider redoing as apure hyperlink instead.</remarks>
        protected void list_ItemCommand(object source, DataListCommandEventArgs e)
        {
            if (e.CommandName == "SelectFacet")
            {
                DataList list = (DataList)source;
                string facetName = list.ID;
                LinkButton link = (LinkButton)e.CommandSource;
                // account for truncation
                string facetValue = (string.IsNullOrEmpty(link.ToolTip)) ? link.Text : link.ToolTip;
                int hits = int.Parse(link.CommandArgument);
                if (_properties[facetName] == null)
                    _properties.Set(facetName, facetValue);
                else
                    _properties[facetName] = facetValue; 
                //this is the part that should be changed if support of multiple facet values per facet to be supported
                // will require revise of SearchQuery too

                RefineSearch();
            }
            //throw new Exception("The method or operation is not implemented.");
        }

        /// <summary>
        /// Constructs the Controls collection of the webpart.
        /// </summary>
        /// <remarks>Consider test against Page.IsPostBack event along with stateful controls. Currently is not implemented</remarks>
        protected override void CreateChildControls()
        {
            base.CreateChildControls();
            try
            {
                if (Context.Request.QueryString != null)
                {
                    SearchQuery query = Utility.ExtractQuery();
                    _k = query.Keywords;
                    _scope = query.Scope;
                    _start = query.Start;
                    _v = query.View;
                    _properties = query.Properties;
                    StringCollection columns = GetFacets();
                    if (!string.IsNullOrEmpty(_hiddenRule))
                    {
                        string key = _hiddenRule.Substring(0, _hiddenRule.IndexOf('=')).Trim();
                        string value = _hiddenRule.Substring(_hiddenRule.IndexOf('=')+1).Trim();
                        _properties.Add(key, value);
                    }
                    SqlBuilder sqlBuilder = new SqlBuilder(query, columns, _prefixEnabled);
                    _sql = sqlBuilder.SqlQuery; 
                    //Controls.Add(new LiteralControl("<div>"+_sql+"</div>"));

                    if (Parent != null && Parent.GetType().FullName == "Microsoft.SharePoint.WebPartPages.WebPartPreview")
                    {
                        // fake something for WP preview only
                        _k = "Microsoft";
                    }
                }

                DataTable dt = (_running2ndTime) ? GetResults(_resultsPerPage) : GetResults(Common.Constants.RESULTS_PER_PAGE_MIN);

                DataList listFacets = CreateFacetsList(dt);
                _facetListId = listFacets.ClientID;

                Controls.Add(listFacets);
            }
            catch (Exception ex)
            {
                Label error = new Label();
                error.EnableViewState = false;
                error.Text = ex.Message;
                error.CssClass = "ms-error";
                Controls.Add(error);
                //throw new ArgumentException(ERROR_CREATING_CONTROLS, ex);
            }
        }

        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            csm = Page.ClientScript;
            if (this.Parent != null)
                _isInPreviewMode = Parent.GetType().FullName == "Microsoft.SharePoint.WebPartPages.WebPartPreview";
            if (!_isInPreviewMode)
            {
                if (this.WebPartManager.DisplayMode == WebPartManager.EditDisplayMode || 
                    this.WebPartManager.DisplayMode == WebPartManager.DesignDisplayMode)
                        _isEditOrDesignMode = true;               
            }
            _run2ndTime = ResultsPerPage > Common.Constants.RESULTS_PER_PAGE_MAX && !_isEditOrDesignMode;
            ValidateColumns();
            InitClientScripts();
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Builds a search string and redirects request using a built querystring.
        /// </summary>
        private void RefineSearch()
        {
            SearchQuery query = new SearchQuery();
            query.Keywords = _k;
            query.Scope = _scope;
            query.Start = _start;
            query.View = _v;
            query.Properties = _properties;

            string qs = Utility.BuildQueryString(query);
            string qsEncoded = string.Format("{0}?{1}", Page.Request.Url.AbsolutePath, qs);
            Page.Response.Redirect(qsEncoded); // consider use of HyperLinks w/o posts
        }

        /// <summary>
        /// Processes received search results and build a facet structure with totals.
        /// </summary>
        /// <param name="dt"></param>
        private void BuildFacets(DataTable dt)
        {
            // our SELECT statement already lists all facets as columns.
            // no need to check, just skip column #1 Path
            StringCollection facets = GetFacets();
            FacetMenu facetMenu = new FacetMenu();

            foreach (DataRow row in dt.Rows)
            { // process each result
                for (int i = 1; i < dt.Columns.Count; i++) // process each meta column except Path
                {
                    string facetName = dt.Columns[i].ColumnName.Trim();
                    if (row[facetName] is string[]) // found multiple values!!!
                    {
                        string[] facetValues = (string[])row[facetName];
                        foreach (string facetValue in facetValues)
                        {
                            AddFacet(facetMenu, facetName, facetValue.Trim());
                        }
                    }
                    else
                    {
                        // prepare for lookup column with multiple selection
                        Regex re = new Regex(Common.Constants.REGEX_LOOKUP_SPLITTER, RegexOptions.Singleline);

                        string facetValue = row[i].ToString().Trim();
                        if (re.IsMatch(facetValue))
                        {
                            string[] facetValues = re.Split(facetValue);
                            foreach (string lookup in facetValues)
                            {
                                if (lookup != null)
                                    AddFacet(facetMenu, facetName, lookup.Trim());
                            }
                        }
                        else
                        {
                            AddFacet(facetMenu, facetName, facetValue);
                        }
                    }
                }
            }
            //remove empty facets so they don't show up in the facet menu
            _facets = facetMenu;
        }


        /// <summary>
        /// Helper method to populate facet menu
        /// </summary>
        /// <param name="facetMenu"></param>
        /// <param name="facetName"></param>
        /// <param name="facetValue"></param>
        private void AddFacet(FacetMenu facetMenu, string facetName, string facetValue)
        {
            if (String.IsNullOrEmpty(facetName.Trim())) return;
            if (String.IsNullOrEmpty(facetValue.Trim()))
            {
                if (_showEmpty)
                    facetValue = Common.Constants.UNNAMED_FACET_VALUE;
                else
                    return;
            }

            if (!facetMenu.ContainsFacetCollection(facetName)) facetMenu.Add(new FacetCollection(facetName.Trim()));

            FacetCollection facetCounter = facetMenu.GetFacetCollection(facetName.Trim());
            Facet facet = facetCounter.GetFacet(facetValue.Trim());
            if (facet == null)
                facetCounter.Add(new Facet(facetValue.Trim(), 1));
            else
                facet.Hits += 1;
        }

        /// <summary>
        /// Executes FullTextSqlQuery with constructed earlier SQL statement and returns
        /// DataTable with all Relevant results.
        /// </summary>
        /// <returns>DataTable</returns><permission cref="System.Security.Permissions.RegistryPermission"></permission>
        private DataTable GetResults(int resultsPerPage)
        {
            ResultTableCollection rtc;

            using (FullTextSqlQuery query = new FullTextSqlQuery(ServerContext.Current))
            {
                query.Hint = QueryHint.OptimizeWithFullTextIndex | QueryHint.OptimizeWithPropertyStore;
                query.KeywordInclusion = KeywordInclusion.AnyKeyword; // or as set in WebPart properties 

                query.EnableStemming = true;
                query.RowLimit = resultsPerPage;
                query.StartRow = 0;
                query.SiteContext = new Uri(SPControl.GetContextSite(this.Context).Url);
                query.TrimDuplicates = true;
                query.EnableStemming = true;
                query.IgnoreAllNoiseQuery = true;
                query.AuthenticationType = QueryAuthenticationType.NtAuthenticatedQuery;
                if (SPSecurity.AuthenticationMode != System.Web.Configuration.AuthenticationMode.Windows)
                {
                    query.AuthenticationType = QueryAuthenticationType.PluggableAuthenticatedQuery;
                }
                query.ResultTypes |= ResultType.RelevantResults;
                query.QueryText = _sql;
                //execute the query using the query object
                rtc = query.Execute();
            }

            DataTable dt = new DataTable();
            if ((rtc != null) && rtc.Exists(ResultType.RelevantResults))
            {
                ResultTable rt = rtc[ResultType.RelevantResults];
                if (rt.RowCount > 0)
                {
                    dt.TableName = rt.ResultType.ToString();
                    dt.Load(rt, LoadOption.OverwriteChanges);
                    _moreResultsThanFacets = rt.TotalRows > Common.Constants.RESULTS_PER_PAGE_MAX;
                }
            }
            if (_run2ndTime && _moreResultsThanFacets)
            {
                if (!csm.IsClientScriptBlockRegistered(this.GetType(), key + "_SetRefineFacets"))
                {
                    csm.RegisterClientScriptBlock(this.GetType(), key + "_SetRefineFacets", "var refineFacets = true;", true);
                }
            }
            //TestFacets(dt); - uncomment wheb need to see the source data
            return dt;
        }

        /// <summary>
        /// Returns collection of columns defined as facets in the webpart UI
        /// </summary>
        /// <remarks>add validation against managed properties</remarks>
        /// <returns></returns>
        private StringCollection GetFacets()
        {

            StringCollection selectedFacetsCollection = new StringCollection();

            //Get selected columns
            _xmlColumns = new XmlDocument();
            try
            {
                if (!string.IsNullOrEmpty(SelectColumns))
                {
                    _xmlColumns.LoadXml(SelectColumns);
                    XmlNodeList selectedFacets = _xmlColumns.GetElementsByTagName("Column");
                    //Populate column name collection
                    foreach (XmlElement column in selectedFacets)
                    {
                        string name = column.GetAttribute("Name");
                        selectedFacetsCollection.Add(name);
                    }
                }
            }
            catch (Exception ex)
            {
                throw new ApplicationException(ERROR_LOADING_XML, ex);
            }
            return selectedFacetsCollection;
        }

        /// <summary>
        /// Returns value of the Column attribute in XML string set by SelectColumns property
        /// </summary>
        /// <param name="attName">name of the attribute</param>
        /// <param name="columnName">name of the column</param>
        /// <returns></returns>

        private string GetColumnAttribute(string columnName, string attName)
        {
            try
            {
                // building smth like \\Column[@Name='Product']@OrderBy
                string xpath = string.Format("//Column[@Name='{0}']/@{1}", columnName, attName);
                XmlNode node = _xmlColumns.SelectSingleNode(xpath);
                if (node == null) return null;
                else return node.Value;
            }
            catch (XPathException ex)
            {
                return null;
            }
        }


        private bool HasExclusion(string columnName)
        {
            try
            {
                string xpath = string.Format("//Column[@Name='{0}']", columnName);
                XmlNode column = _xmlColumns.SelectSingleNode(xpath);
                if (!column.HasChildNodes) return false;
                return column.SelectNodes("Exclusions") != null;
            }
            catch (Exception ex)
            {
                return false;
            }
        }
        private bool Excluded(string columnName, string facetName)
        {
            try
            {
                //string xpath = string.Format("//Column[@Name='{0}']/Exclusions/Exclusion/@Match", columnName);
                string xpath = string.Format("//Column[@Name='{0}']/Exclusions/Exclusion[(contains(@Match, '%') and starts-with('{1}', substring-before(@Match, '%'))) or (@Match = '{1}')]/@{2}", columnName, facetName, "Match");
                // add loop thru exclusions
                XmlNode node = _xmlColumns.SelectSingleNode(xpath);
                if (node != null)
                {
                    string pattern = node.Value;
                    return Utility.IsMatch(pattern, facetName);
                }
                return false;
            }
            catch (Exception ex)
            {
                return false;
            }
        }
        private void ApplyExclusions()
        {
            for (int j=0; j<_facets.Count; j++)
            {
                FacetCollection col = _facets[j];
                if (!HasExclusion(col.Name)) continue;
                for (int i=0; i<col.Count; i++)
                {
                    Facet facet = col[i];
                    if (Excluded(col.Name, facet.Name))
                    {
                        col.Remove(facet);
                        i--;
                    }
                }
                if (col.Count == 0) _facets.Remove(col); // removing empty facet collections
            }
        }

        private bool HasMapping(string columnName)
        {
            try
            {
                string xpath = string.Format("//Column[@Name='{0}']", columnName);
                XmlNode column = _xmlColumns.SelectSingleNode(xpath);
                if (!column.HasChildNodes) return false;
                return column.SelectNodes("Mappings") != null;
            }
            catch (XPathException ex)
            {
                return false;
            }
        }        

        /// <summary>
        /// Retruns value of the Column attribute
        /// </summary>
        /// <param name="columnName">Column name</param>
        /// <param name="facetName">Facet name (@Name attribute)</param>
        /// <param name="attName">Atribute name</param>
        /// <returns></returns>
        private string GetMapping(string columnName, string facetName, string attName)
        {
            try
            {
                //string xpath = string.Format("//Column[@Name='{0}']/Mappings/Mapping[starts-with('{1}',substring-before(@Match, '%'))]/@{2}", columnName, facetName, attName);
                /* the following line define XPATH expression. The goal is to provide different processing for
                 * wild-card Matches (where Match parameter ends with '%', but I check contains as there is no ends-with function in XSL1.0)
                 * and regular parameter that implies explicit equal                 
                 * */
                string xpath = string.Format("//Column[@Name='{0}']/Mappings/Mapping[(contains(@Match, '%') and starts-with('{1}', substring-before(@Match, '%'))) or (@Match = '{1}')]/@{2}", columnName, facetName, attName);
                    
                XmlNode node = _xmlColumns.SelectSingleNode(xpath);
                
                if (node == null) return null;
                else return node.Value;
            }
            catch (Exception ex)
            {
                return null;
            }
        }
        
        private void ApplyMappings()
        {
            for (int j = 0; j < _facets.Count; j++)
            {
                FacetCollection col = _facets[j];
                FacetCollection clone = (FacetCollection) col.Clone();
                if (!HasMapping(col.Name)) continue;
                string facetDefaultImage = GetColumnAttribute(col.Name, "DefaultImage");
                for (int i = 0; i < col.Count; i++)
                {
                    Facet facet = col[i];
                    if (!string.IsNullOrEmpty(facetDefaultImage)) facet.Image = facetDefaultImage;

                    string match = GetMapping(col.Name, facet.Name, "Match");
                    if (!string.IsNullOrEmpty(match))
                    {
                        string displayName = GetMapping(col.Name, facet.Name, "DisplayName");
                        if (!string.IsNullOrEmpty(displayName)) facet.DisplayName = displayName;

                        string facetImage = GetMapping(col.Name, facet.Name, "Image");
                        if (!string.IsNullOrEmpty(facetImage)) facet.Image = facetImage;

                        //if (match.EndsWith("%"))
                        //{
                        //    string prefix = match.TrimEnd('%');
                        //    foreach (Facet f in clone) {
                        //        if (f.Name.StartsWith(prefix) && (f.Name != facet.Name)) // consider use Culture based comparison
                        //            facet.Hits += f.Hits;
                        //    }
                        //}
                    }                    
                }
            }
        }

        /// <summary>
        /// Returns value of the Columns attribute in XML string set by SelectColumns property
        /// </summary>
        /// <param name="attName">name of the attribute</param>
        /// <returns></returns>
        private string GetColumnsAttribute(string attName)
        {
            try
            {
                XmlNode columns = _xmlColumns.DocumentElement.ChildNodes[0];
                if (columns.Attributes[attName] != null) return columns.Attributes[attName].Value;
                return null;
            }
            catch (XPathException ex)
            {
                return null;
            }
        }

        /// <summary>
        /// A test method used to display Grid with search results for testing
        /// </summary>
        /// <param name="dt"></param>
        private void TestFacets(DataTable dt)
        {
            GridView view = new GridView();
            view.DataSource = dt;
            view.DataBind();
            view.GridLines = GridLines.Both;
            Controls.Add(view);
        }

        private void InitClientScripts()
        { 
            if (!csm.IsClientScriptIncludeRegistered(key))
            {
                //+++++++ LEAVE FOR FUTURE REFACTORING TO EMBEDDED RESOURCES +++++++//
                //string scriptName = Common.Constants.SCRIPT_NAME;
                //System.Reflection.ManifestResourceInfo mri = this.GetType().Assembly.GetManifestResourceInfo(scriptName);
                //Debug.Assert(null != mri);
                //string scriptUrl = csm.GetWebResourceUrl(this.GetType(), scriptName);
                //csm.RegisterClientScriptResource(this.GetType(), scriptName);
                string scriptUrl = "/_layouts/FacetedSearch.js";
                csm.RegisterClientScriptInclude(this.GetType(), key, scriptUrl);
            }
            if (!csm.IsClientScriptBlockRegistered(key))
            {
                #region Common scripts
                if (!csm.IsStartupScriptRegistered(this.GetType(), key + "_ProcessFacetTables"))
                {
                    csm.RegisterStartupScript(this.GetType(), key + "_ProcessFacetTables", "processFacetTables();", true);
                }
                #endregion

                #region 2nd run scripts
                if (_run2ndTime)
                {
                    Control ctrl = FindControl("Facets");
                    if (ctrl == null) return;
                    if (!csm.IsClientScriptBlockRegistered(this.GetType(), key + "_CallBack"))
                    {
                        string cbReference = csm.GetCallbackEventReference(this, "arg", "processServerData", "context", null, false);
                        string callBack = @"
function callServer(arg, context) {
    try {" +
           cbReference +

@";
    } catch(e) {
        alert(e.message);
    }
}
                        
function processServerData(arg, context) {
    //setProgress('/_layouts/images/progressdone.gif');
    var facets = document.getElementById('" + ctrl.ClientID + @"');
    if (facets) {
        facets.outerHTML = arg;
        processFacetTables();
    }
    //setProgress('/_layouts/images/blank.gif');
}                                  
                        ";
                        csm.RegisterClientScriptBlock(this.GetType(), key + "_CallBack", callBack, true);
                    }

                    if (!csm.IsStartupScriptRegistered(this.GetType(), key + "_CallServer"))
                    {
                        csm.RegisterStartupScript(this.GetType(), key + "_CallServer", "if (refineFacets) callServer('" + ctrl.ClientID + "', '34');", true);
                    }
                }
                #endregion
            }


        }

        private DataList CreateFacetsList(DataTable dt)
        {
            BuildFacets(dt);
            ApplyExclusions();
            ApplyMappings();

            DataList listFacets = new DataList();
            listFacets.ID = "Facets";
            listFacets.ItemDataBound += new DataListItemEventHandler(rpt_ItemDataBound);
            listFacets.CssClass = _listCss;
            FacetMenu facetMenu = _facets;
            // adding sorting
            string orderBy = GetColumnsAttribute("OrderBy");
            if (!string.IsNullOrEmpty(orderBy))
            {
                try
                {
                    FacetSortEnum facetsOrderBy = FacetSortEnum.None;
                    EnumConverter converter = (EnumConverter)TypeDescriptor.GetConverter(facetsOrderBy);
                    facetsOrderBy = (FacetSortEnum)converter.ConvertFromString(orderBy);
                    facetMenu.SortOrder = facetsOrderBy;
                    facetMenu.Sort(facetMenu);
                }
                catch
                {
                    throw new NotSupportedException(ERROR_CONVERTING_ORDERBY);
                }
            }

            listFacets.DataSource = facetMenu;
            listFacets.DataBind();
            return listFacets;
        }

        private void ValidateColumns()
        {
            try
            {
                ServerContext cntxt = ServerContext.Current;
                SearchContext context = SearchContext.GetContext(cntxt);
                Schema schema = new Schema(context);
                ManagedPropertyCollection properties = schema.AllManagedProperties;
                StringCollection facets = GetFacets();
                foreach (string facet in facets)
                {
                    if (!properties.Contains(facet))
                    {
                        // do something
                        //throw new ApplicationException(string.Concat(ERROR_LOADING_FACETS, facet));
                        string msg = string.Concat(ERROR_LOADING_FACETS, facet);
                        Label error = new Label();
                        error.CssClass = "ms-error";
                        error.Text = msg;
                        Controls.Add(error);
                        break;
                    }
                }
            }
            catch (Exception ex)
            {
                string msg = ex.Message;
                Label error = new Label();
                error.CssClass = "ms-error";
                error.Text = ex.Message;
                Controls.Add(error);
            }
        }
        #endregion

        #region ICallbackEventHandler Members

        public string GetCallbackResult()
        {
            return _facetHtml;
        }

        public void RaiseCallbackEvent(string eventArgument)
        {
            if (_run2ndTime && _moreResultsThanFacets)
            {
                _running2ndTime = true;
                SecondRun();
            }
        }

        private void SecondRun()
        {
            DataTable dt = GetResults(_resultsPerPage);
            //BuildFacets(dt);
            //ApplyExclusions();
            //ApplyMappings();
            DataList listFacets = CreateFacetsList(dt);
            Controls.Add(listFacets);
            _facetHtml = GetHtmlFromControl(listFacets);
            Controls.Remove(listFacets);
        }

        private string GetHtmlFromControl(Control ctrl)
        {
            StringBuilder result = new StringBuilder(1024);
            ctrl.RenderControl(new HtmlTextWriter(new StringWriter(result)));
            return result.ToString();
        }
        #endregion

        [ConnectionProvider("Facet Settings", "FacetSettings", AllowsMultipleConnections = true)]
        public XmlNode FacetSettings()
        {
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(_selectColumns);
            return doc.DocumentElement;
        }
    }

}
